/**
 * 实现拖拽行为
 */
import { truthy, getLogger } from '@gnlab/utils'
import throttle from 'lodash/throttle'
import { getGroupElement, parseGroupId, getAllGroups, limit } from './helpers'
import s from './index.module.less'

const logger = getLogger('ui/choose-groups/drag')

/**
 * 常量定义
 */
const AUTO_SCROLLING_STEP = 30 // 滚动条自动滚动时，一次滚动多少像素
const AUTO_SCROLLING_INTERVAL = 30 // 滚动条自动滚动时，两次滚动的间隔

/**
 * 拖拽过程中的数据
 */
interface DraggingInfo {
  draggingGroupId: string // 被拖拽的 group 的 id
  mouseYHistory: number[] // 上一次记录的鼠标 Y 坐标
  groupList: GroupInfo[] // 所有 group item 元素集合
  placeholder: HTMLElement // placeholder 元素
  scrollingIntervalId: NodeJS.Timeout | null // 自动滚动容器的定时 ID
  highlightAboveElement: HTMLElement | null // 当前高亮的元素（移动到此元素的上面）
  highlightBelowElement: HTMLElement | null // 当前高亮的元素（移动到此元素的下面）
}

interface GroupInfo {
  id: string
  element: HTMLElement // group element
  top: number // 元素顶部位置
  bottom: number // 元素底部位置
}

type DraggingInfoRef = React.MutableRefObject<DraggingInfo | undefined>

function updateInfo(ref: DraggingInfoRef, updates: Partial<DraggingInfo>) {
  const current = ref.current ?? ({} as DraggingInfo)
  ref.current = { ...current, ...updates }
}

/**
 * 生成 placeholder
 */
function makePlaceholder(initialGroupElement: HTMLElement, initialMouseY: number) {
  const parent = initialGroupElement.parentElement!
  const width = initialGroupElement.offsetWidth

  const placeholder = document.createElement('div')
  placeholder.innerHTML = '群'
  placeholder.className = s.placeholder!
  placeholder.style.width = `${width}px`
  parent.appendChild(placeholder)
  updatePlaceholderTop(placeholder, initialMouseY)

  requestAnimationFrame(() => {
    placeholder.className += ` ${s.visible!}`
  })

  return placeholder
}

/**
 * 计算当前 placeholder 应有的 top 值
 */
function updatePlaceholderTop(placeholder: HTMLElement, mouseY: number) {
  const parent = placeholder.parentElement!
  // 理想的 top 位置是让 placeholder 的中线处于鼠标位置
  const expectTop =
    mouseY -
    parent.getBoundingClientRect().top +
    parent.scrollTop -
    Math.round(placeholder.offsetHeight / 2)
  const limitedTop = limitPlaceholderTop(placeholder, expectTop)
  placeholder.style.top = `${limitedTop}px`
  return limitedTop
}

/**
 * 限制 placeholder 的 top 值，使其保持在视口内
 */
function limitPlaceholderTop(placeholder: HTMLElement, expectTop: number) {
  const parent = placeholder.parentElement!

  const limited = limit(
    parent.scrollTop, // top 不能小于已滚动的距离，不然 placeholder 会跑到视口上边界之外
    expectTop,
    parent.scrollTop + parent.offsetHeight - placeholder.offsetHeight, // top 要在下方留出 placeholder 的高度，不然会跑到视口下边界之外
  )

  return limited
}

/**
 * 处理容器元素自动滚动（即在 placeholder 到达视口边缘时，滚动视口）
 */
function handleAutoScrolling(infoRef: DraggingInfoRef, placeholderTop: number) {
  const { placeholder } = infoRef.current!

  stopAutoScrolling(infoRef)

  const parent = placeholder.parentElement!
  let up: boolean | null = null // null 代表不移动
  // 移动到了视口顶部，但没移动到整个容器的顶部
  if (placeholderTop === parent.scrollTop && parent.scrollTop !== 0) up = true
  // 移动到了视口底部，但没移动到整个容器的底部
  else if (
    placeholderTop === parent.scrollTop + parent.offsetHeight - placeholder.offsetHeight &&
    parent.scrollTop + parent.offsetHeight < parent.scrollHeight
  )
    up = false
  if (up === null) return

  const scroll = scrollParent.bind(null, placeholder, up)
  const intervalId = setInterval(scroll, AUTO_SCROLLING_INTERVAL)
  updateInfo(infoRef, { scrollingIntervalId: intervalId })
  scroll()
}

/**
 * 终止自动滚动
 */
function stopAutoScrolling(infoRef: DraggingInfoRef) {
  const { scrollingIntervalId: prevScrollingIntervalId } = infoRef.current!
  if (prevScrollingIntervalId) {
    clearInterval(prevScrollingIntervalId)
    updateInfo(infoRef, { scrollingIntervalId: null })
    logger.debug('stop auto scrolling')
  }
}

/**
 * 执行单次自动滚动
 * up: 为 true 时向上滚动；否则向下滚动
 */
function scrollParent(placeholder: HTMLElement, up: boolean) {
  const parent = placeholder.parentElement!
  requestAnimationFrame(() => {
    const top = up
      ? Math.max(parent.scrollTop - AUTO_SCROLLING_STEP, 0)
      : Math.min(parent.scrollTop + AUTO_SCROLLING_STEP, parent.scrollHeight - parent.offsetHeight)
    const topChange = top - parent.scrollTop
    parent.scrollTo(0, top)

    const placeholderTop = parseInt(placeholder.style.top, 10) + topChange
    placeholder.style.top = `${placeholderTop}px`

    logger.debug(`auto scrolling ${up ? 'up' : 'down'}`, top)
  })
}

/**
 * 让被拖拽的元素高亮
 */
function makeDraggingHighlight(infoRef: DraggingInfoRef) {
  const { draggingGroupId, groupList } = infoRef.current!
  const element = groupList.find(item => item.id === draggingGroupId)!.element
  element.className += ' dragging'
}

/**
 * 给被拖拽的元素取消高亮
 */
function removeDraggingHighlight(infoRef: DraggingInfoRef) {
  const { draggingGroupId, groupList } = infoRef.current!
  const element = groupList.find(item => item.id === draggingGroupId)!.element
  element.className = element.className.replace('dragging', '')
}

/**
 * 处理元素高亮
 */
const handleHighlight = throttle((infoRef: DraggingInfoRef) => {
  const draggingResult = calcuteDraggingResult(infoRef)
  if (!draggingResult) removeHighlight(infoRef)
  else makeHighlight(infoRef, draggingResult.besideGroup.element, draggingResult.up)
}, 100)

/**
 * 高亮元素
 */
function makeHighlight(infoRef: DraggingInfoRef, element: HTMLElement, above: boolean) {
  removeHighlight(infoRef)
  element.className += ` highlight ${above ? 'above' : 'below'}`
  if (above) updateInfo(infoRef, { highlightAboveElement: element })
  else updateInfo(infoRef, { highlightBelowElement: element })
}

/**
 * 取消高亮
 */
function removeHighlight(infoRef: DraggingInfoRef) {
  const { highlightAboveElement, highlightBelowElement } = infoRef.current!
  const element = highlightAboveElement ?? highlightBelowElement
  if (element)
    element.className = element.className
      .replace('highlight', '')
      .replace('above', '')
      .replace('below', '')
  updateInfo(infoRef, { highlightAboveElement: null, highlightBelowElement: null })
}

/**
 * 计算拖拽结果
 * mouseY: 当前鼠标纵坐标
 * 返回 null 代表没有移动
 */
function calcuteDraggingResult(infoRef: DraggingInfoRef) {
  if (!infoRef.current) return null
  const { placeholder, mouseYHistory, draggingGroupId, groupList } = infoRef.current
  if (!placeholder.parentElement) return null

  // 鼠标向上动时基于上边缘判断，向下动时基于下边缘判断
  const mouseUp = confirmSideByMouseYHistory(mouseYHistory)
  const placeholderTop = parseInt(placeholder.style.top, 10)
  const placeholderEdge = mouseUp ? placeholderTop : placeholderTop + placeholder.offsetHeight

  // 确定 placeholder 落在哪个 group 上
  // 若没落在任何 placeholder，则说明是出现在容器末尾（因为容易末尾会有空白，placeholder 在空白中时就没落在 group 上）
  const overGroup = groupList.find(
    item => item.top <= placeholderEdge && item.bottom >= placeholderEdge,
  )

  // up: 向 besideGroup 的上面还是下面放置
  const [up, besideGroup] = overGroup
    ? [mouseUp, overGroup]
    : [false, groupList[groupList.length - 1]!]

  // 目标放置位置的 index
  const placeIndex = groupList.indexOf(besideGroup) + (up ? -1 : 1)
  if (besideGroup.id === draggingGroupId || groupList[placeIndex]?.id === draggingGroupId) {
    logger.debug('排序位置没有变动')
    return null
  }

  return { besideGroup, up, placeIndex }
}

/**
 * 根据 mouseYHistory 确定鼠标的运动趋势是向上还是向下的
 * 返回 true 代表向上，false 代表向下
 */
function confirmSideByMouseYHistory(history: number[]) {
  if (history.length === 1) return true
  if (history.length <= 4) return history[history.length - 1]! <= history[history.length - 2]!

  let moved = 0
  for (let i = 1; i < history.length; i++) {
    moved += history[i]! - history[i - 1]!
  }
  return moved <= 0
}

/**
 * 核心拖拽行为
 */
export interface DroppedInfo {
  draggingId: string
  prevId: string | null // 目标群移动到哪个群后面，为 null 代表移动到最前面
}
export function useDragActions(onDropped: (info: DroppedInfo) => void) {
  const infoRef = useRef<DraggingInfo>() // 有值说明已开始拖拽

  function dragStart(e: MouseEvent) {
    const initialGroupElement = getGroupElement(e.target as HTMLElement)
    const draggingGroupId = initialGroupElement && parseGroupId(initialGroupElement)
    const groupElements = initialGroupElement && getAllGroups(initialGroupElement)
    if (!initialGroupElement || !truthy(draggingGroupId) || !groupElements) return

    const initialMouseY = e.clientY
    const placeholder = makePlaceholder(initialGroupElement, initialMouseY)
    const groupList = [...groupElements].map(([id, element]) => ({
      id,
      element,
      top: element.offsetTop,
      bottom: element.offsetTop + element.offsetHeight,
    }))
    infoRef.current = {
      draggingGroupId,
      mouseYHistory: [initialMouseY],
      groupList,
      placeholder,
      scrollingIntervalId: null,
      highlightAboveElement: null,
      highlightBelowElement: null,
    }

    makeDraggingHighlight(infoRef)
    logger.debug('drag start', draggingGroupId)
  }

  function dragMove(e: MouseEvent) {
    const { placeholder } = infoRef.current!

    const mouseYHistory = [...infoRef.current!.mouseYHistory, e.clientY].slice(-10)
    updateInfo(infoRef, { mouseYHistory })

    const placeholderTop = updatePlaceholderTop(placeholder, e.clientY)
    handleAutoScrolling(infoRef, placeholderTop)
    handleHighlight(infoRef)
  }

  function dragFinish() {
    const { draggingGroupId, placeholder, groupList } = infoRef.current!
    logger.debug('drag finish', draggingGroupId)

    const draggingResult = calcuteDraggingResult(infoRef)

    stopAutoScrolling(infoRef)
    removeHighlight(infoRef)
    removeDraggingHighlight(infoRef)
    placeholder.parentElement!.removeChild(placeholder)

    if (draggingResult) {
      const { besideGroup, up } = draggingResult
      const besideIndex = groupList.indexOf(besideGroup)
      const prevGroup = up ? groupList[besideIndex - 1] : besideGroup
      onDropped({
        draggingId: draggingGroupId,
        prevId: prevGroup?.id ?? null,
      })
    }
  }

  return {
    dragStart,
    dragMove,
    dragFinish,
  }
}
